/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License. You may obtain a
 * copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.ljt.openapi.demo.util;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.CoreConnectionPNames;

import com.ljt.openapi.demo.Response;
import com.ljt.openapi.demo.constant.Constants;
import com.ljt.openapi.demo.constant.ContentType;
import com.ljt.openapi.demo.constant.HttpHeader;
import com.ljt.openapi.demo.constant.HttpMethod;
import com.ljt.openapi.demo.constant.SystemHeader;

/**
 * Http工具类
 */
public class HttpUtil {

  private static final Log logger = LogFactory.getLog(HttpUtil.class);


  /**
   * HTTP GET
   * 
   * @param host
   * @param path
   * @param connectTimeout
   * @param headers
   * @param querys
   * @param signHeaderPrefixList
   * @param appKey
   * @param appSecret
   * @return
   * @throws Exception
   */
  public static Response httpGet(String host, String path, int connectTimeout,
      Map<String, String> headers, Map<String, String> querys, List<String> signHeaderPrefixList,
      String appKey, String appSecret) throws Exception {
    headers = initialBasicHeader(HttpMethod.GET, path, headers, querys, null, signHeaderPrefixList,
        appKey, appSecret);

    HttpClient httpClient = wrapClient(host);

    httpClient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT,
        getTimeout(connectTimeout));

    HttpGet get = new HttpGet(initUrl(host, path, querys));

    for (Map.Entry<String, String> e : headers.entrySet()) {
      get.addHeader(e.getKey(), MessageDigestUtil.utf8ToIso88591(e.getValue()));
    }

    return convert(httpClient.execute(get));
  }

  /**
   * HTTP POST表单
   * 
   * @param host
   * @param path
   * @param connectTimeout
   * @param headers
   * @param querys
   * @param bodys
   * @param signHeaderPrefixList
   * @param appKey
   * @param appSecret
   * @return
   * @throws Exception
   */
  public static Response httpPost(String host, String path, int connectTimeout,
      Map<String, String> headers, Map<String, String> querys, Map<String, String> bodys,
      List<String> signHeaderPrefixList, String appKey, String appSecret) throws Exception {
    if (headers == null) {
      headers = new HashMap<String, String>();
    }

    headers.put(HttpHeader.HTTP_HEADER_CONTENT_TYPE, ContentType.CONTENT_TYPE_FORM);

    headers = initialBasicHeader(HttpMethod.POST, path, headers, querys, bodys,
        signHeaderPrefixList, appKey, appSecret);

    HttpClient httpClient = wrapClient(host);
    httpClient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT,
        getTimeout(connectTimeout));

    HttpPost post = new HttpPost(initUrl(host, path, querys));
    for (Map.Entry<String, String> e : headers.entrySet()) {
      post.addHeader(e.getKey(), MessageDigestUtil.utf8ToIso88591(e.getValue()));
    }

    UrlEncodedFormEntity formEntity = buildFormEntity(bodys);
    if (formEntity != null) {
      post.setEntity(formEntity);
    }
    return convert(httpClient.execute(post));
  }

  /**
   * Http POST 字符串
   * 
   * @param host
   * @param path
   * @param connectTimeout
   * @param headers
   * @param querys
   * @param body
   * @param signHeaderPrefixList
   * @param appKey
   * @param appSecret
   * @return
   * @throws Exception
   */
  public static Response httpPost(String host, String path, int connectTimeout,
      Map<String, String> headers, Map<String, String> querys, String body,
      List<String> signHeaderPrefixList, String appKey, String appSecret, boolean isSend)
      throws Exception {
    headers = initialBasicHeader(HttpMethod.POST, path, headers, querys, null, signHeaderPrefixList,
        appKey, appSecret);

    HttpClient httpClient = wrapClient(host);
    httpClient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT,
        getTimeout(connectTimeout));

    HttpPost post = new HttpPost(initUrl(host, path, querys));
    for (Map.Entry<String, String> e : headers.entrySet()) {
      post.addHeader(e.getKey(), MessageDigestUtil.utf8ToIso88591(e.getValue()));
    }

    if (StringUtils.isNotBlank(body)) {
      post.setEntity(new StringEntity(body, Constants.ENCODING));

    }
    logPost(post);
    if (isSend) {

      return convert(httpClient.execute(post));
    } else {
      return null;
    }
  }

  private static void logPost(HttpPost post) {
    StringBuilder log = new StringBuilder();
    log.append("send request:");
    log.append(post.toString());
    log.append(Constants.LF);
    Header[] headers = post.getAllHeaders();
    log.append("headers:");
    log.append(Constants.LF);
    for (int i = 0; i < headers.length; i++) {

      log.append(headers[i].getName() + ":" + headers[i].getValue());
      log.append(Constants.LF);
    }

    HttpEntity entity = post.getEntity();
    if (entity == null) {

      log.append("body is emputy");
      logger.info(log.toString());
      return;
    }

    log.append("body:");

    log.append(Constants.LF);
    try {
      List<String> entitys =
          org.apache.commons.io.IOUtils.readLines(entity.getContent(), Constants.ENCODING);
      for (Iterator<String> it = entitys.iterator(); it.hasNext();) {
        log.append(it.next());
      }
    } catch (UnsupportedOperationException | IOException e) {
      logger.warn(e.getMessage(), e);
    }
    logger.info(log.toString());
  }

  /**
   * HTTP POST 字节数组
   * 
   * @param host
   * @param path
   * @param connectTimeout
   * @param headers
   * @param querys
   * @param bodys
   * @param signHeaderPrefixList
   * @param appKey
   * @param appSecret
   * @return
   * @throws Exception
   */
  public static Response httpPost(String host, String path, int connectTimeout,
      Map<String, String> headers, Map<String, String> querys, byte[] bodys,
      List<String> signHeaderPrefixList, String appKey, String appSecret) throws Exception {
    headers = initialBasicHeader(HttpMethod.POST, path, headers, querys, null, signHeaderPrefixList,
        appKey, appSecret);

    HttpClient httpClient = wrapClient(host);
    httpClient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT,
        getTimeout(connectTimeout));

    HttpPost post = new HttpPost(initUrl(host, path, querys));
    for (Map.Entry<String, String> e : headers.entrySet()) {
      post.addHeader(e.getKey(), MessageDigestUtil.utf8ToIso88591(e.getValue()));
    }

    if (bodys != null) {
      post.setEntity(new ByteArrayEntity(bodys));
    }

    return convert(httpClient.execute(post));
  }

  /**
   * HTTP PUT 字符串
   * 
   * @param host
   * @param path
   * @param connectTimeout
   * @param headers
   * @param querys
   * @param body
   * @param signHeaderPrefixList
   * @param appKey
   * @param appSecret
   * @return
   * @throws Exception
   */
  public static Response httpPut(String host, String path, int connectTimeout,
      Map<String, String> headers, Map<String, String> querys, String body,
      List<String> signHeaderPrefixList, String appKey, String appSecret) throws Exception {
    headers = initialBasicHeader(HttpMethod.PUT, path, headers, querys, null, signHeaderPrefixList,
        appKey, appSecret);

    HttpClient httpClient = wrapClient(host);
    httpClient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT,
        getTimeout(connectTimeout));

    HttpPut put = new HttpPut(initUrl(host, path, querys));
    for (Map.Entry<String, String> e : headers.entrySet()) {
      put.addHeader(e.getKey(), MessageDigestUtil.utf8ToIso88591(e.getValue()));
    }

    if (StringUtils.isNotBlank(body)) {
      put.setEntity(new StringEntity(body, Constants.ENCODING));

    }

    return convert(httpClient.execute(put));
  }

  /**
   * HTTP PUT字节数组
   * 
   * @param host
   * @param path
   * @param connectTimeout
   * @param headers
   * @param querys
   * @param bodys
   * @param signHeaderPrefixList
   * @param appKey
   * @param appSecret
   * @return
   * @throws Exception
   */
  public static Response httpPut(String host, String path, int connectTimeout,
      Map<String, String> headers, Map<String, String> querys, byte[] bodys,
      List<String> signHeaderPrefixList, String appKey, String appSecret) throws Exception {
    headers = initialBasicHeader(HttpMethod.PUT, path, headers, querys, null, signHeaderPrefixList,
        appKey, appSecret);

    HttpClient httpClient = wrapClient(host);
    httpClient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT,
        getTimeout(connectTimeout));

    HttpPut put = new HttpPut(initUrl(host, path, querys));
    for (Map.Entry<String, String> e : headers.entrySet()) {
      put.addHeader(e.getKey(), MessageDigestUtil.utf8ToIso88591(e.getValue()));
    }

    if (bodys != null) {
      put.setEntity(new ByteArrayEntity(bodys));
    }

    return convert(httpClient.execute(put));
  }

  /**
   * HTTP DELETE
   * 
   * @param host
   * @param path
   * @param connectTimeout
   * @param headers
   * @param querys
   * @param signHeaderPrefixList
   * @param appKey
   * @param appSecret
   * @return
   * @throws Exception
   */
  public static Response httpDelete(String host, String path, int connectTimeout,
      Map<String, String> headers, Map<String, String> querys, List<String> signHeaderPrefixList,
      String appKey, String appSecret) throws Exception {
    headers = initialBasicHeader(HttpMethod.DELETE, path, headers, querys, null,
        signHeaderPrefixList, appKey, appSecret);

    HttpClient httpClient = wrapClient(host);
    httpClient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT,
        getTimeout(connectTimeout));

    HttpDelete delete = new HttpDelete(initUrl(host, path, querys));
    for (Map.Entry<String, String> e : headers.entrySet()) {
      delete.addHeader(e.getKey(), MessageDigestUtil.utf8ToIso88591(e.getValue()));
    }

    return convert(httpClient.execute(delete));
  }

  /**
   * 构建FormEntity
   * 
   * @param formParam
   * @return
   * @throws UnsupportedEncodingException
   */
  private static UrlEncodedFormEntity buildFormEntity(Map<String, String> formParam)
      throws UnsupportedEncodingException {
    if (formParam != null) {
      List<NameValuePair> nameValuePairList = new ArrayList<NameValuePair>();

      for (String key : formParam.keySet()) {
        nameValuePairList.add(new BasicNameValuePair(key, formParam.get(key)));
      }
      UrlEncodedFormEntity formEntity =
          new UrlEncodedFormEntity(nameValuePairList, Constants.ENCODING);
      formEntity.setContentType(ContentType.CONTENT_TYPE_FORM);
      return formEntity;
    }

    return null;
  }

  private static String initUrl(String host, String path, Map<String, String> querys)
      throws UnsupportedEncodingException {
    StringBuilder sbUrl = new StringBuilder();
    sbUrl.append(host);
    if (!StringUtils.isBlank(path)) {
      sbUrl.append(path);
    }
    if (null != querys) {
      StringBuilder sbQuery = new StringBuilder();
      for (Map.Entry<String, String> query : querys.entrySet()) {
        if (0 < sbQuery.length()) {
          sbQuery.append(Constants.SPE3);
        }
        if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) {
          sbQuery.append(query.getValue());
        }
        if (!StringUtils.isBlank(query.getKey())) {
          sbQuery.append(query.getKey());
          if (!StringUtils.isBlank(query.getValue())) {
            sbQuery.append(Constants.SPE4);
            sbQuery.append(URLEncoder.encode(query.getValue(), Constants.ENCODING));
          }
        }
      }
      if (0 < sbQuery.length()) {
        sbUrl.append(Constants.SPE5).append(sbQuery);
      }
    }

    return sbUrl.toString();
  }


  /**
   * 初始化基础Header
   * 
   * @param method
   * @param path
   * @param headers
   * @param querys
   * @param bodys
   * @param signHeaderPrefixList
   * @param appKey
   * @param appSecret
   * @return
   * @throws MalformedURLException
   */
  private static Map<String, String> initialBasicHeader(String method, String path,
      Map<String, String> headers, Map<String, String> querys, Map<String, String> bodys,
      List<String> signHeaderPrefixList, String appKey, String appSecret)
      throws MalformedURLException {
    if (headers == null) {
      headers = new HashMap<String, String>();
    }

    headers.put(SystemHeader.X_CA_TIMESTAMP, String.valueOf(new Date().getTime()));
    // headers.put(SystemHeader.X_CA_NONCE, UUID.randomUUID().toString());
    headers.put(SystemHeader.X_CA_KEY, appKey);
    headers.put(SystemHeader.X_CA_SIGNATURE,
        SignUtil.sign(appSecret, method, path, headers, querys, bodys, signHeaderPrefixList));

    return headers;
  }

  /**
   * 读取超时时间
   * 
   * @param timeout
   * @return
   */
  private static int getTimeout(int timeout) {
    if (timeout == 0) {
      return Constants.DEFAULT_TIMEOUT;
    }

    return timeout;
  }

  private static Response convert(HttpResponse response) throws IOException {
    StringBuilder log = new StringBuilder();
    Response res = new Response();

    if (null != response) {
      res.setStatusCode(response.getStatusLine().getStatusCode());
      log.append("response stats code=" + res.getStatusCode());
      log.append(Constants.LF);
      log.append("headers:");
      log.append(Constants.LF);
      for (Header header : response.getAllHeaders()) {
        String headerValue = MessageDigestUtil.iso88591ToUtf8(header.getValue());
        log.append(header.getName() + ":" + headerValue);
        log.append(Constants.LF);
        res.setHeader(header.getName(), headerValue);
      }

      res.setContentType(res.getHeader("Content-Type"));
      res.setRequestId(res.getHeader("X-Ca-Request-Id"));
      res.setErrorMessage(res.getHeader("X-Ca-Error-Message"));
      res.setBody(readStreamAsStr(response.getEntity().getContent()));
      log.append("response body:" + res.getBody());
      logger.info(log);
    } else {
      logger.warn("server No Response!");
      // 服务器无回应
      res.setStatusCode(500);
      res.setErrorMessage("No Response");
    }

    return res;
  }


  /**
   * 将流转换为字符串
   * 
   * @param is
   * @return
   * @throws IOException
   */
  public static String readStreamAsStr(InputStream is) throws IOException {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    WritableByteChannel dest = Channels.newChannel(bos);
    ReadableByteChannel src = Channels.newChannel(is);
    ByteBuffer bb = ByteBuffer.allocate(4096);

    while (src.read(bb) != -1) {
      bb.flip();
      dest.write(bb);
      bb.clear();
    }
    src.close();
    dest.close();

    return new String(bos.toByteArray(), Constants.ENCODING);
  }

  private static HttpClient wrapClient(String host) {
    HttpClient httpClient = new DefaultHttpClient();
    if (host.startsWith("https://")) {
      sslClient(httpClient);
    }

    return httpClient;
  }

  private static void sslClient(HttpClient httpClient) {
    try {
      SSLContext ctx = SSLContext.getInstance("TLS");
      X509TrustManager tm = new X509TrustManager() {
        public X509Certificate[] getAcceptedIssuers() {
          return null;
        }

        public void checkClientTrusted(X509Certificate[] xcs, String str) {

        }

        public void checkServerTrusted(X509Certificate[] xcs, String str) {

        }
      };
      ctx.init(null, new TrustManager[] {tm}, null);
      SSLSocketFactory ssf = new SSLSocketFactory(ctx);
      ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
      ClientConnectionManager ccm = httpClient.getConnectionManager();
      SchemeRegistry registry = ccm.getSchemeRegistry();
      registry.register(new Scheme("https", 443, ssf));
    } catch (KeyManagementException ex) {
      throw new RuntimeException(ex);
    } catch (NoSuchAlgorithmException ex) {
      throw new RuntimeException(ex);
    }
  }
}
